/*
 * Copyright 2009 Armando Blancas
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package taskgraph.reflect;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;


/**
 * Provides static methods related to Java Bean reflection.
 * 
 * @author Armando Blancas
 */
public class BeanMetadata {
	
	static {
		/*
		 * Register property editor for wrapper classes so that they may
		 * also be used for values of their own type, e.g., when client bean
		 * code declares an Integer instead of int.
		 */
		PropertyEditor editor = editorFor(Boolean.TYPE);		
		PropertyEditorManager.registerEditor(Boolean.class, editor.getClass());
		editor = editorFor(Byte.TYPE);		
		PropertyEditorManager.registerEditor(Byte.class, editor.getClass());
		editor = editorFor(Short.TYPE);		
		PropertyEditorManager.registerEditor(Short.class, editor.getClass());
		editor = editorFor(Integer.TYPE);		
		PropertyEditorManager.registerEditor(Integer.class, editor.getClass());
		editor = editorFor(Long.TYPE);		
		PropertyEditorManager.registerEditor(Long.class, editor.getClass());
		editor = editorFor(Float.TYPE);		
		PropertyEditorManager.registerEditor(Float.class, editor.getClass());
		editor = editorFor(Double.TYPE);		
		PropertyEditorManager.registerEditor(Double.class, editor.getClass());
		
		/*
		 * Declare local property editors.
		 */
		PropertyEditorManager.registerEditor(
			Character.TYPE, CharEditor.class);
		PropertyEditorManager.registerEditor(
			Character.class, CharEditor.class);
		PropertyEditorManager.registerEditor(
			BigInteger.class, BigIntegerEditor.class);
		PropertyEditorManager.registerEditor(
			BigDecimal.class, BigDecimalEditor.class);
		PropertyEditorManager.registerEditor(
			java.util.Date.class, DateEditor.class);
		PropertyEditorManager.registerEditor(
			java.sql.Date.class, SqlDateEditor.class);
		PropertyEditorManager.registerEditor(
			java.sql.Time.class, SqlTimeEditor.class);
		PropertyEditorManager.registerEditor(
			java.sql.Timestamp.class, SqlTimestampEditor.class);
	}
	
	/**
	 * Convenience method for getting the property editor for {@code clazz}.
	 * 
	 * @param clazz The class of the value we want to edit.
	 * @return The property editor for instances of that class.
	 */
	public static PropertyEditor editorFor(Class<?> clazz) { 
		return PropertyEditorManager.findEditor(clazz);
	}

	/**
	 * Gets getter methods from a class.
	 * 
	 * Uses Bean Introspection to retrieve the getter methods in {@code clazz}
	 * optionally annotated with a position value, which defaults to zero.
	 * 
	 * <p>This method will ignore any properties without a {@code @Field} 
	 * annotation in one of its accessors; and also those that do not define 
	 * both a getter and a setter. Thus this method is not suitable as a 
	 * generic extractor of T getT() methods.
	 *  
	 * @return A {@code Accessor} array of getters for the {@code clazz} argument.
	 * @throws IntrospectionException If introspection fails.
	 */
	public static Accessor[] extractGetters(Class<?> clazz) 
	throws IntrospectionException {
		List<Accessor> list = new LinkedList<Accessor>();
		BeanInfo info = Introspector.getBeanInfo(clazz);
		PropertyDescriptor[] desc = info.getPropertyDescriptors();
		for (PropertyDescriptor pd : desc) {
			Method getm = pd.getReadMethod();
			Method setm = pd.getWriteMethod();
			if (getm == null || setm == null) {
				continue;
			}
			int order = 0;
			if (getm.isAnnotationPresent(Field.class)) {
				order = getm.getAnnotation(Field.class).position();
				if (order == 0) {
					if (setm.isAnnotationPresent(Field.class)) {
						order = setm.getAnnotation(Field.class).position();
					}
				}
			}
			if (order >= 0) {
				list.add(new Accessor(order, pd.getName(), getm));
			}
		}
		return list.toArray(new Accessor[0]);
	}

	/**
	 * Gets getter methods from a class matching a database table.
	 * 
	 * Uses Bean Introspection to retrieve the getter methods in {@code clazz}
	 * optionally annotated with {@code Column} data, usually the column name.
	 * This method is used for JDBC access.
	 * 
	 * <p>The resulting array contains accessors that have a corresponding
	 * column number in a database table, found by matching the accessor's
	 * annotated column name or its property name (default).
	 * 
	 * @param clazz The class object to be introspected.
	 * @param set   The result set with column names metadata. 
	 * @return An {@code Accessor} array for the {@code clazz} argument.
	 * @throws IntrospectionException If introspection fails.
	 */
	public static Accessor[] extractGetters(Class<?> clazz, ResultSet set) 
	throws IntrospectionException {
		List<Accessor> list = new LinkedList<Accessor>();
		BeanInfo info = Introspector.getBeanInfo(clazz);
		PropertyDescriptor[] desc = info.getPropertyDescriptors();
		for (PropertyDescriptor pd : desc) {
			Method getm = pd.getReadMethod();
			Method setm = pd.getWriteMethod();
			if (getm == null || setm == null) {
				continue;
			}
			// Lookup the column name from annotations
			String name = null;
			if (getm.isAnnotationPresent(Column.class)) {
				name = getm.getAnnotation(Column.class).name();
				if (name == null) {
					if (setm.isAnnotationPresent(Column.class)) {
						name = getm.getAnnotation(Column.class).name();
					}
				}
			}
			// No annotation means use the property name as column name
			if (name == null) {
				name = pd.getName();
			}

			// Lookup column# from column name
			try {
				int position = set.findColumn(name);
				list.add(new Accessor(position, name, getm));
			} catch (SQLException e) {
				// ignore accessors without column#
			} 
		}
		return list.toArray(new Accessor[0]);
	}
	
	/**
	 * Gets setter methods from a class.
	 * 
	 * Uses Bean Introspection to retrieve the setter methods in {@code clazz}
	 * optionally annotated with a position value, which defaults to zero. 
	 * {@code Field} annotations are used for data source that do note have
	 * explicit metadata, like text and binary flat files.
	 * 
	 * 
	 * <p>This method will ignore any properties without a {@code @Field} 
	 * annotation in one of its accessors; and also those that do not define 
	 * both a getter and a setter. Thus this method is not suitable as a 
	 * generic extractor of void setT(T t) methods.
	 *
	 * @param clazz The class object to be introspected.
	 * @return An {@code Accessor} array of setters for the {@code clazz} argument.
	 * @throws IntrospectionException If introspection fails.
	 * @see taskgraph.reflect.Field
	 */
	public static Accessor[] extractSetters(Class<?> clazz) 
	throws IntrospectionException {
		List<Accessor> list = new LinkedList<Accessor>();
		BeanInfo info = Introspector.getBeanInfo(clazz);
		PropertyDescriptor[] desc = info.getPropertyDescriptors();
		for (PropertyDescriptor pd : desc) {
			Method getm = pd.getReadMethod();
			Method setm = pd.getWriteMethod();
			if (getm == null || setm == null) {
				continue;
			}
			int order = 0;
			if (getm.isAnnotationPresent(Field.class)) {
				order = getm.getAnnotation(Field.class).position();
				if (order == 0) {
					if (setm.isAnnotationPresent(Field.class)) {
						order = setm.getAnnotation(Field.class).position();
					}
				}
			}
			if (order >= 0) {
				list.add(new Accessor(order, pd.getName(), setm));
			}
		}
		return list.toArray(new Accessor[0]);
	}

	/**
	 * Gets setter methods from a class.
	 * 
	 * Uses Bean Introspection to retrieve the setter methods in {@code clazz}
	 * optionally annotated with {@code Column} data, usually the column name.
	 * This method is used for JDBC access or any other module that will
	 * produce a result set.
	 * 
	 * <p>The resulting array contains accessors that have a corresponding
	 * column number in the result set, found by matching the accessor's
	 * annotated column name or its property name (default).
	 * 
	 * @param clazz The class object to be introspected.
	 * @param set   The result set with metadata for matching accessor. 
	 * @return An {@code Accessor} array for the {@code clazz} argument.
	 * @throws IntrospectionException If introspection fails.
	 */
	public static Accessor[] extractSetters(Class<?> clazz, ResultSet set) 
	throws IntrospectionException {
		List<Accessor> list = new LinkedList<Accessor>();
		BeanInfo info = Introspector.getBeanInfo(clazz);
		PropertyDescriptor[] desc = info.getPropertyDescriptors();
		for (PropertyDescriptor pd : desc) {
			Method getm = pd.getReadMethod();
			Method setm = pd.getWriteMethod();
			if (getm == null || setm == null) {
				continue;
			}
			// Lookup the column name from annotations
			String name = null;
			if (getm.isAnnotationPresent(Column.class)) {
				name = getm.getAnnotation(Column.class).name();
				if (name == null) {
					if (setm.isAnnotationPresent(Column.class)) {
						name = getm.getAnnotation(Column.class).name();
					}
				}
			}
			// No annotation means use the property name as column name
			if (name == null) {
				name = pd.getName();
			}

			// Lookup column# from column name
			try {
				int position = set.findColumn(name);
				list.add(new Accessor(position, name, setm));
			} catch (SQLException e) {
				// ignore accessors without column#
			} 
		}
		return list.toArray(new Accessor[0]);
	}

	/**
	 * Gets the table name that corresponds to the passed class.
	 * This class will have an @Entity annotation with the name of table.
	 * 
	 * @param clazz The class object to introspect.
	 * @return A table name or null if none is defined.
	 */
	public static String getEntity(Class<?> clazz) {
		String name = null;
		if (clazz.isAnnotationPresent(Entity.class)) {
			name = clazz.getAnnotation(Entity.class).name();
			if (name.length() == 0) {
				name = null;
			}
		}
		return name;
	}

	/**
	 * Creates an instance and sets its properties.
	 * 
	 * Creates a new instance of clazz and initializes it by calling each of 
	 * the passed setter methods, using the corresponding value coming
	 * from a Delimited Text file as strings.
	 * 
	 * <p>String values are converted to the right type for the setter call
	 * to succeed. The initialized instance is returned to the caller.
	 * 
	 * @param clazz     The class to instantiate.
	 * @param accessors An array of {@code OrderedMethod} elements from clazz.
	 * @param resultSet The result set with column values. 
	 * @return The initialized instance of E {@code clazz}.
	 * @throws InstantiationException If clazz cannot be instantiated.
	 * @throws IllegalAccessException If this code has no visibility to clazz.
	 * @throws InvocationTargetException If the setter cannot be called.
	 * @throws SQLException 
	 */
	public static <T> T newInstance(Class<T> clazz, 
			                        Accessor[] accessors, 
			                        ResultSet resultSet)
	throws InstantiationException,   
		   IllegalAccessException, 
	       InvocationTargetException, 
	       SQLException {
		T instance = clazz.newInstance();
		for (Accessor accessor : accessors) {
			int column = accessor.getPosition();
			Method setter = accessor.getMethod();
			setter.invoke(instance, resultSet.getObject(column));
			
		}
		return instance;
	}
	
	/**
	 * Creates an instance and sets its properties.
	 * 
	 * Creates a new instance of clazz and initializes it by calling each of 
	 * the passed setter methods, using the corresponding value coming
	 * from a Delimited Text file as strings.
	 * 
	 * <p>String values are converted to the right type for the setter call
	 * to succeed. The initialized instance is returned to the caller.
	 * 
	 * @param clazz   The class to instantiate.
	 * @param setters An array of {@code OrderedMethod} elements from clazz.
	 * @param values  An array of values to set, as {@code String}s.
	 * @return The initialized instance of E {@code clazz}.
	 * @throws InstantiationException If clazz cannot be instantiated.
	 * @throws IllegalAccessException If this code has no visibility to clazz.
	 * @throws InvocationTargetException If the setter cannot be called.
	 * @throws IllegalArgumentException  If the argument to a setter is invalid.
	 */
	public static <T> T newInstance(Class<T> clazz, 
			                        Accessor[] setters, 
			                        String[] values)
	throws InstantiationException,   
		   IllegalAccessException, 
	       IllegalArgumentException, 
	       InvocationTargetException {
		
		T instance = clazz.newInstance();
		for (Accessor accessor : setters) {
			int position = accessor.getPosition() - 1;
			if (position < values.length) {
				Method set = accessor.getMethod();
				Class<?> paramType = set.getParameterTypes()[0];
				PropertyEditor editor = editorFor(paramType);
				if (editor != null) {
					editor.setAsText(values[position]);
					set.invoke(instance, editor.getValue());
				}
			}
		}
		return instance;
	}
	
}
